package com.agua.enturno.modelo.agenda;

import com.agua.enturno.excepciones.ExcepcionDeReglaDelNegocio;
import com.agua.enturno.modelo.ReglasDelNegocio;
import com.agua.enturno.modelo.roles.Cliente;
import com.agua.enturno.modelo.roles.Notificacion;
import com.agua.enturno.modelo.turno.EstadoDeTurno;
import com.agua.enturno.modelo.turno.Horario;
import com.agua.enturno.modelo.turno.Turno;
import com.agua.enturno.util.DateFormatter;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.Interval;

import java.util.*;

/**
 * Una jornada laboral define uno o más intervalos de tiempo en los cuales el prestador 
 * de servicios atiende turnos de manera ininterrumpida.
 *
 * @author Victor Del Rio
 */
public abstract class JornadaLaboral {

	private Long id;
	protected Agenda agenda;
	
	protected Duration duracionDeLosTurnos;
	protected Integer vacantesPorTurno;
	
	
	/*------------------------------------ Constructores -----------------------------------*/

	protected JornadaLaboral() {
		super();
	}
	
	public JornadaLaboral(Duration duracionDeLosTurnos, Integer vacantesPorTurno) {
		
		this.duracionDeLosTurnos = duracionDeLosTurnos;
		this.vacantesPorTurno = vacantesPorTurno;
	}
	
	/*--------------------------------------------------------------------------------------*/
	/*------------------------------------ Public methods ----------------------------------*/
	
	public SortedSet<Turno> getTurnos(EstadoDeTurno estado) {
		
		SortedSet<Turno> turnos = new TreeSet<Turno>();
		
		for (Horario horario : this.getHorarios()) {
			turnos.addAll(horario.getTurnos(estado));
		}
		
		return turnos;
	}
	
	public void notificarALosClientes(String mensaje) {
		
		for (Horario horario : this.getHorarios()) {
			horario.notificarALosClientes(mensaje);
		}
	}
	
	public void modificarDuracionDeLosTurnos(Duration nuevaDuracion) throws ExcepcionDeReglaDelNegocio {
		
		if (!this.duracionDeLosTurnos.equals(nuevaDuracion)) {
			
			ReglasDelNegocio.laDuracionDeLosTurnosDebeSerMayorALaMinimaPermitida(nuevaDuracion);
			ReglasDelNegocio.laDuracionDeLosTurnosDebeSerCoherenteConLaDeLaJornada(nuevaDuracion, this.getDuracionDeLosIntervalos());
		
			this.duracionDeLosTurnos = nuevaDuracion;
			this.regenerarHorariosSegunDuracion();
		}
	}
	
	public void modificarVacantesPorTurno(Integer vacantes, String motivo) throws ExcepcionDeReglaDelNegocio {
		
		int diferenciaEnVacantes = vacantes - this.getVacantesPorTurno();
		
		if (diferenciaEnVacantes > 0)
			this.agregarVacantesPorTurno(diferenciaEnVacantes);
		else if (diferenciaEnVacantes < 0)
			this.quitarVacantesPorTurno(-diferenciaEnVacantes, motivo);
	}
	
	public void agregarVacantesPorTurno(Integer vacantesAAgregar) throws ExcepcionDeReglaDelNegocio {
		
		for (Horario horario : this.getHorarios()) 
			horario.agregarTurnos(vacantesAAgregar);
		
		this.vacantesPorTurno += vacantesAAgregar;
	}
	
	public void quitarVacantesPorTurno(Integer vacantesAQuitar, String motivo) throws ExcepcionDeReglaDelNegocio {
		
		for (Horario horario : this.getHorarios()) 
			horario.quitarTurnos(vacantesAQuitar, motivo);
		
		this.vacantesPorTurno -= vacantesAQuitar; 
	}
	
	/*--------------------------------------------------------------------------------------*/
	/*----------------------------------- Abstract methods ---------------------------------*/
	
	public abstract SortedSet<Horario> getHorarios();
	
	public abstract void actualizarHorarios() throws ExcepcionDeReglaDelNegocio;
	
	protected abstract Duration getDuracionDeLosIntervalos();
	
	protected abstract void ligarA(Agenda agenda) throws ExcepcionDeReglaDelNegocio;
	
	protected abstract void desligarDe(Agenda agenda, String motivo) throws ExcepcionDeReglaDelNegocio;
	
	/*--------------------------------------------------------------------------------------*/
	/*---------------------------------- Geters and seters ---------------------------------*/

	public Long getId() {
		return id;
	}

	@SuppressWarnings("unused")
	private void setId(Long id) {
		this.id = id;
	}
	
	public Agenda getAgenda() {
		return agenda;
	}
	
	public Duration getDuracionDeLosTurnos() {
		return duracionDeLosTurnos;
	}

	public Integer getVacantesPorTurno() {
		return vacantesPorTurno;
	}

	/*--------------------------------------------------------------------------------------*/
	/*---------------------------------- Protected methods ---------------------------------*/

	protected void borrarHorarios(String motivo) {
		
		Turno.cancelarTurnos(this.getTurnos(EstadoDeTurno.OCUPADO), motivo);
		this.agenda.getHorarios().removeAll(this.getHorarios());
	}
	
	protected boolean estaLigada() {
		return this.agenda != null;
	}

	/*--------------------------------------------------------------------------------------*/
	/*----------------------------------- Private methods ----------------------------------*/
	
	private void regenerarHorariosSegunDuracion() throws ExcepcionDeReglaDelNegocio {
		
		Map<DateTime, List<Cliente>> turnosAnteriores = this.salvarTurnosActules();
		Agenda agenda = this.agenda;
		
		this.desligarDe(this.agenda, null);
		this.ligarA(agenda);
			
		this.reubicarClientes(turnosAnteriores);
	}

	private void reubicarClientes(Map<DateTime, List<Cliente>> turnosAnteriores) {
		
		for (Map.Entry<DateTime, List<Cliente>> entry : turnosAnteriores.entrySet()) {
			
			DateTime inicioDelTurno = entry.getKey();
			Interval intervalo = new Interval(inicioDelTurno, this.duracionDeLosTurnos);
			Horario horario = this.agenda.getHorario(intervalo);
			
			// Sigue existiendo el mismo comienzo de horario
			if (horario != null) {
				
				try {
					for (Cliente cliente : entry.getValue()) {
						
						horario.sacarTurno(cliente);
						notificarDuracionDeTurno(cliente, inicioDelTurno);
					}
				} catch (ExcepcionDeReglaDelNegocio e) {}
			}
			else {
				for (Cliente cliente : entry.getValue()) 
					notificarTurnoCanceladoPorCambioDeDuracionDeTurnos(cliente, inicioDelTurno);
			}
		}
	}
	
	private Map<DateTime, List<Cliente>> salvarTurnosActules() {
		
		Map<DateTime, List<Cliente>> clientes = new TreeMap<DateTime, List<Cliente>>();
		
		for (Horario horario : this.getHorarios()) {
			
			List<Cliente> clientesEnHorario = horario.getClientes();
			if (!clientesEnHorario.isEmpty())
				clientes.put(horario.getIntervalo().getStart(), clientesEnHorario);
		}
				
		return clientes;
	}
	
	private void notificarTurnoCanceladoPorCambioDeDuracionDeTurnos(Cliente cliente, DateTime dateTime) {
		
		String nombreDelPrestador = this.agenda.getPrestadorDeServicios().getNombre();
		String nombreDeLaAgenda = this.agenda.getNombre();
		String fechaDelTurno = DateFormatter.formatDateTime(dateTime, true);
				
		cliente.getUsuario().getNotificaciones().add(
				new Notificacion("Su turno del " + fechaDelTurno + " en " + nombreDelPrestador + " para la agenda " + nombreDeLaAgenda
						+ " ha sido cancelado debido a una reestructuración en la duración de los turnos de dicha agenda."));
	}
	
	private void notificarDuracionDeTurno(Cliente cliente, DateTime dateTime) {
		
		String nombreDelPrestador = this.agenda.getPrestadorDeServicios().getNombre();
		String nombreDeLaAgenda = this.agenda.getNombre();
		String fechaDelTurno = DateFormatter.formatDateTime(dateTime, true);
		
		cliente.getUsuario().getNotificaciones().add(
				new Notificacion("Ha sido modificada la duración de su turno del " + fechaDelTurno + " en " + nombreDelPrestador + " para la agenda " + nombreDeLaAgenda
						+ ". La nueva duración del mísmo es de " + DateFormatter.formatDuration(this.duracionDeLosTurnos)));
	}
	
	/*--------------------------------------------------------------------------------------*/
	
}
